/*
 * Copyright (c) 2018, apexes.net. All rights reserved.
 *
 *         http://www.apexes.net
 *
 */
package net.apexes.codegen.core;

import com.querydsl.codegen.EntityType;
import com.querydsl.codegen.Property;
import com.querydsl.codegen.Serializer;
import com.querydsl.codegen.SerializerConfig;
import com.querydsl.codegen.TypeMappings;
import com.querydsl.codegen.utils.CodeWriter;
import com.querydsl.codegen.utils.Symbols;
import com.querydsl.codegen.utils.model.ClassType;
import com.querydsl.codegen.utils.model.Parameter;
import com.querydsl.codegen.utils.model.SimpleType;
import com.querydsl.codegen.utils.model.Type;
import com.querydsl.codegen.utils.model.TypeCategory;
import com.querydsl.codegen.utils.model.Types;
import com.querydsl.sql.ColumnMetadata;
import com.querydsl.sql.codegen.NamingStrategy;
import com.querydsl.sql.codegen.SQLCodegenModule;
import com.querydsl.sql.codegen.support.ForeignKeyData;
import com.querydsl.sql.codegen.support.KeyData;
import net.apexes.commons.lang.Checks;
import net.apexes.commons.lang.Enume;
import net.apexes.commons.lang.Strings;
import net.apexes.commons.ormlite.Column;
import net.apexes.commons.ormlite.ForeignKey;
import net.apexes.commons.ormlite.Index;
import net.apexes.commons.querydsl.info.IndexColumn;

import javax.inject.Inject;
import javax.inject.Named;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;

/**
 * @author <a href="mailto:hedyn@foxmail.com">HeDYn</a>
 */
public class OrmliteColumnMetadataSerializer implements Serializer {

    private static final String FK_VARIABLE = "fk";
    private static final String IX_VARIABLE = "ix";

    protected final TypeMappings typeMappings;
    private final NamingStrategy namingStrategy;
    private final JdbcTypeConverter jdbcTypeConverter;
    protected final boolean innerClassesForKeys;
    protected final Collection<String> keywords;
    private final Set<String> imports;
    private final Comparator<Property> columnComparator;
    private final Class<?> superClass;

    @Inject
    public OrmliteColumnMetadataSerializer(TypeMappings typeMappings,
                                           NamingStrategy namingStrategy,
                                           JdbcTypeConverter jdbcTypeConverter,
                                           @Named(SQLCodegenModule.INNER_CLASSES_FOR_KEYS) boolean innerClassesForKeys,
                                           @Named(SQLCodegenModule.IMPORTS) Set<String> imports,
                                           @Named(SQLCodegenModule.COLUMN_COMPARATOR) Comparator<Property> columnComparator,
                                           @Named(SQLCodegenModule.ENTITYPATH_TYPE) Class<?> superClass) {
        this.typeMappings = typeMappings;
        this.namingStrategy = namingStrategy;
        this.jdbcTypeConverter = jdbcTypeConverter;
        this.innerClassesForKeys = innerClassesForKeys;
        this.keywords = Collections.emptyList();
        this.imports = new HashSet<>(imports);
        this.columnComparator = columnComparator;
        this.superClass = superClass;
    }

    @SuppressWarnings("unchecked")
    @Override
    public void serialize(EntityType model, SerializerConfig config, CodeWriter writer) throws IOException {
        Type supertype = new SimpleType(TypeCategory.ENTITY,
                superClass.getName(),
                superClass.getPackage().getName(),
                superClass.getSimpleName(), false, false, model);

        List<Property> properties = CodegenUtils.getProperties(model, superClass, columnComparator);
        Collection<ForeignKeyData> foreignKeys = (Collection<ForeignKeyData>) model.getData().get(ForeignKeyData.class);
        Collection<IndexData> indexs = (Collection<IndexData>) model.getData().get(IndexData.class);

        Type queryType = typeMappings.getPathType(model, model, false);
        if (!queryType.getPackageName().isEmpty()) {
            writer.packageDecl(queryType.getPackageName());
        }

        Set<String> importClasses = new TreeSet<>();
        importClasses.add(supertype.getFullName());
        if (!model.getPackageName().isEmpty() && !queryType.getPackageName().equals(model.getPackageName())
                && !queryType.getSimpleName().equals(model.getSimpleName())) {
            String fullName = model.getFullName();
            String packageName = model.getPackageName();
            if (fullName.substring(packageName.length() + 1).contains(".")) {
                fullName = fullName.substring(0, fullName.lastIndexOf('.'));
            }
            importClasses.add(fullName);
        }
        importClasses.add(Column.class.getName());
        for (Property property : properties) {
            Class<?> classType = property.getType().getJavaClass();
            if (Enume.class.isAssignableFrom(classType)) {
                importClasses.add(classType.getName());
            }
        }
        if (innerClassesForKeys) {
            if (Checks.isNotEmpty(indexs)) {
                importClasses.add(Index.class.getName());
            }
            if (Checks.isNotEmpty(foreignKeys)) {
                importClasses.add(ForeignKey.class.getName());
            }
        }

        writer.importClasses(importClasses.toArray(new String[0]));
        writeUserImports(writer);

        introJavadoc(writer, model);
        introClassHeader(writer, model, supertype);
        if (config.createDefaultVariable()) {
            introDefaultInstance(writer, model, config.defaultVariableName());
        }

        serializeProperties(model, properties, writer);

        if (innerClassesForKeys) {
            if (Checks.isNotEmpty(indexs)) {
                serializeIndex(model, indexs, writer);
            }
            if (Checks.isNotEmpty(foreignKeys)) {
                serializeForeignKeys(model, foreignKeys, writer);
            }
        }

        // constructors
        constructors(model, config, writer);

        addNewInstance(model, writer);
        addGetFieldValue(model, properties, writer);
        addSetupFieldValue(model, properties, writer);

        outro(model, writer);
    }

    protected void writeUserImports(CodeWriter writer) throws IOException {
        Set<String> packages = new HashSet<>();
        Set<String> classes = new HashSet<>();

        for (String javaImport : imports) {
            //true if the character next to the dot is an upper case or if no dot is found (-1+1=0) the first character
            boolean isClass = Character.isUpperCase(javaImport.charAt(javaImport.lastIndexOf(".") + 1));
            if (isClass) {
                classes.add(javaImport);
            } else {
                packages.add(javaImport);
            }
        }

        if (!packages.isEmpty()) {
            writer.importPackages(packages.toArray(new String[0]));
        }
        if (!classes.isEmpty()) {
            writer.importClasses(classes.toArray(new String[0]));
        }
    }

    protected void introJavadoc(CodeWriter writer, EntityType model) throws IOException {
        Type queryType = typeMappings.getPathType(model, model, true);
        writer.javadoc(queryType.getSimpleName() + " is a ORMLite query type for " + model.getSimpleName(),
                "@see " + model.getSimpleName());
    }

    protected void introClassHeader(CodeWriter writer, EntityType model, Type superType) throws IOException {
        for (Annotation annotation : model.getAnnotations()) {
            writer.annotation(annotation);
        }
        Type queryType = typeMappings.getPathType(model, model, true);
        writer.beginClass(queryType, superType);
    }

    protected void introDefaultInstance(CodeWriter writer, EntityType entityType, String defaultName)
            throws IOException {
        String variableName = !defaultName.isEmpty() ? defaultName : namingStrategy.getDefaultVariableName(entityType);
        Type queryType = typeMappings.getPathType(entityType, entityType, true);
        String table = (String) entityType.getData().get("table");
        writer.publicStaticFinal(queryType, variableName, Symbols.NEW + queryType.getSimpleName() + "(\"" + table + "\")");
    }

    protected void serializeProperties(EntityType model, List<Property> properties, CodeWriter writer) throws IOException {
        // fields
        Type colmumType = new SimpleType(Column.class.getSimpleName());
        StringBuilder sb = new StringBuilder();
        for (Property property : properties) {
            ColumnMetadata metadata = getColumnMetadata(property);
            int jdbcType = metadata.getJdbcType();
            if (jdbcTypeConverter != null) {
                jdbcType = jdbcTypeConverter.convert(jdbcType);
            }
            String propertyName = property.getEscapedName();
            sb.setLength(0);
            sb.append("field(\"").append(propertyName).append("\")");
            sb.append(".columnName(\"").append(metadata.getName()).append("\")");
            Class<?> classType = property.getType().getJavaClass();
            if (Enume.class.isAssignableFrom(classType)) {
                switch (jdbcType) {
                    case java.sql.Types.INTEGER:
                        sb.append(".enumeInt(").append(classType.getSimpleName()).append(".class)");
                        break;
                    case java.sql.Types.CHAR:
                        sb.append(".enumeChar(").append(classType.getSimpleName()).append(".class, ")
                                .append(metadata.getSize()).append(")");
                        break;
                    case java.sql.Types.VARCHAR:
                        sb.append(".enumeString(").append(classType.getSimpleName()).append(".class, ")
                                .append(metadata.getSize()).append(")");
                        break;
                    default:
                        break;
                }
            } else {
                switch (jdbcType) {
                    case java.sql.Types.CHAR:
                        sb.append(".character(").append(metadata.getSize()).append(")");
                        break;
                    case java.sql.Types.VARCHAR:
                        sb.append(".varchar(").append(metadata.getSize()).append(")");
                        break;
                    case java.sql.Types.DECIMAL:
                        if (Integer.class.isAssignableFrom(classType)) {
                            sb.append(".integer()");
                        } else if (Long.class.isAssignableFrom(classType)) {
                            sb.append(".bigint()");
                        } else {
                            sb.append(".decimal(").append(metadata.getSize()).append(", ").append(metadata.getDigits()).append(")");
                        }
                        break;
                    case java.sql.Types.INTEGER:
                        sb.append(".integer()");
                        break;
                    case java.sql.Types.BIGINT:
                        sb.append(".bigint()");
                        break;
                    case java.sql.Types.DATE:
                        if (LocalDate.class == classType) {
                            sb.append(".localDate()");
                        } else {
                            sb.append(".date()");
                        }
                        break;
                    case java.sql.Types.TIMESTAMP:
                    case java.sql.Types.TIMESTAMP_WITH_TIMEZONE:
                        if (LocalDateTime.class == classType) {
                            sb.append(".localDateTime()");
                        } else {
                            sb.append(".timestamp()");
                        }
                        break;
                    case java.sql.Types.TIME:
                    case java.sql.Types.TIME_WITH_TIMEZONE:
                        if (LocalDateTime.class == classType) {
                            sb.append(".localTime()");
                        } else {
                            sb.append(".time()");
                        }
                        break;
                    case java.sql.Types.BINARY:
                    case java.sql.Types.BLOB:
                    case java.sql.Types.CLOB:
                        sb.append(".byteArray()");
                        break;
                    default:
                        break;
                }
            }
            if (!metadata.isNullable()) {
                sb.append(".notNull()");
            }

            sb.append(".build()");
//            writer.javadoc(metadata.getName(), "@see " + model.getSimpleName() + "#" + propertyName);
            writer.publicFinal(colmumType, propertyName, sb.toString());
        }
    }

    protected void serializeForeignKeys(EntityType model, Collection<? extends KeyData> foreignKeys, CodeWriter writer) throws IOException {
        Type foreignKeysType = new SimpleType("ForeignKeys");
        writer.beginClass(foreignKeysType);

        StringBuilder value = new StringBuilder();
        Type foreignKeyType = new ClassType(ForeignKey.class);
        for (KeyData foreignKey : foreignKeys) {
            value.setLength(0);
            value.append("foreignKey(");
            value.append(namingStrategy.getPropertyName(foreignKey.getForeignColumns().get(0), model));
            value.append(", \"").append(foreignKey.getTable()).append("\"");
            value.append(", \"").append(foreignKey.getParentColumns().get(0)).append("\"");
            value.append(")");

            String fieldName = namingStrategy.getPropertyNameForForeignKey(foreignKey.getName(), model);
            writer.publicFinal(foreignKeyType, fieldName, value.toString());
        }

        writer.end();
        writer.publicFinal(foreignKeysType, FK_VARIABLE, "new " + foreignKeysType.getSimpleName() + "()");
    }

    protected void serializeIndex(EntityType model,  Collection<IndexData> indexs, CodeWriter writer) throws IOException {
        Type indexsType = new SimpleType("Indexs");
        writer.beginClass(indexsType);

        Type indexType = new ClassType(Index.class);
        for (IndexData index : indexs) {
            String indexName = index.getName();
            String fieldName = namingStrategy.getPropertyNameForPrimaryKey(indexName, model);
            StringBuilder value = new StringBuilder("index(\"").append(indexName).append("\")");
            if (index.isUnique()) {
                value.append(".unique()");
            }
            for (IndexColumn indexColumn : index.getColumns()) {
                String columnName = namingStrategy.getPropertyName(indexColumn.getColumnName(), model);
                value.append(".column(").append(columnName);
                if (indexColumn.isDesc()) {
                    value.append(", ").append(true);
                }
                value.append(")");
            }
            value.append(".build()");
            writer.publicFinal(indexType, fieldName, value.toString());
        }

        writer.end();
        writer.publicFinal(indexsType, IX_VARIABLE, "new " + indexsType.getSimpleName() + "()");
    }

    private ColumnMetadata getColumnMetadata(Property property) {
        return (ColumnMetadata) property.getData().get("COLUMN");
    }

    protected void constructors(EntityType model, SerializerConfig config, CodeWriter writer)
            throws IOException {
        String localName = writer.getRawName(model);
        writer.beginConstructor(new Parameter("tableName", Types.STRING));
        writer.line("super(", writer.getClassConstant(localName), ", tableName);");
        constructorContent(writer, model);
        writer.end();
    }

    protected void constructorContent(CodeWriter writer, EntityType model) throws IOException {
        // override in subclasses
    }

    protected void addNewInstance(EntityType model, CodeWriter writer) throws IOException {
        writer.annotation(Override.class);
        writer.beginPublicMethod(model, "newInstance");
        writer.line("return new ", model.getSimpleName(), "();");
        writer.end();
    }

    protected void addGetFieldValue(EntityType model, List<Property> properties, CodeWriter writer) throws IOException {
        Parameter instanceParam = new Parameter("instance", model);
        Parameter fieldNameParam = new Parameter("fieldName", Types.STRING);
        writer.annotation(Override.class);
        writer.beginPublicMethod(Types.OBJECT, "getFieldValue", instanceParam, fieldNameParam);
        writer.line("switch (fieldName) {");
        for (Property property : properties) {
            String name = property.getName();
            writer.line("    case \"", name, "\":");
            writer.line("        return instance.get", Strings.capitalize(name), "();");
        }
        writer.line("    default:");
        writer.line("        return super.getFieldValue(instance, fieldName);");
        writer.line("}");
        writer.end();
    }

    protected void addSetupFieldValue(EntityType model, List<Property> properties, CodeWriter writer) throws IOException {
        Parameter instanceParam = new Parameter("instance", model);
        Parameter fieldNameParam = new Parameter("fieldName", Types.STRING);
        Parameter valueParam = new Parameter("value", Types.OBJECT);
        writer.annotation(Override.class);
        writer.beginPublicMethod(Types.VOID, "setupFieldValue", instanceParam, fieldNameParam, valueParam);
        writer.line("switch (fieldName) {");
        for (Property property : properties) {
            String name = property.getName();
            String typeFullName = property.getType().getFullName();
            writer.line("    case \"", name, "\":");
            writer.line("        instance.set", Strings.capitalize(name), "((", typeFullName, ") value);");
            writer.line("        break;");
        }
        writer.line("    default:");
        writer.line("        super.setupFieldValue(instance, fieldName, value);");
        writer.line("        break;");
        writer.line("}");
        writer.end();
    }

    protected void outro(EntityType model, CodeWriter writer) throws IOException {
        writer.end();
    }

}